// --------------------------------------------------------------------------------------------------------------------- 
// <copyright file="Extensions.cs" company="Scrum for Team System">
//   None
// </copyright>
// <summary>
//   Defines the Extensions type.
// </summary>
// ---------------------------------------------------------------------------------------------------------------------
namespace ScrumForTeamSystem.EventService.AggregationService
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;

    using DataObjects;

    using Infrastructure;

    using Microsoft.TeamFoundation.WorkItemTracking.Client;

    /// <summary>
    /// Defines the Extensions type.
    /// </summary>
    internal static class Extensions
    {
        /// <summary>
        /// Determines whether [the specified work item changed event] [has path changes].
        /// </summary>
        /// <param name="workItemChangedEvent">The work item changed event.</param>
        /// <returns>
        /// <c>true</c> if [the specified work item changed event] [has path changes]; otherwise, <c>false</c>.
        /// </returns>
        public static bool HasPathChanges(this WorkItemChangedEvent workItemChangedEvent)
        {
            return
                    workItemChangedEvent.ChangedFields.StringFields.Any(
                            sf => 
                            sf.ReferenceName.Equals(ConstantStrings.WorkItemIterationPathCoreField)
                            || sf.ReferenceName.Equals(ConstantStrings.WorkItemAreaPathCoreField));
        }

        /// <summary>
        /// Determines whether [the specified work item changed event] [has an altered source field name].
        /// </summary>
        /// <param name="workItemChangedEvent">The work item changed event.</param>
        /// <param name="rule">The calculation rule.</param>
        /// <returns>
        /// <c>true</c> if [the specified work item changed event] [has an altered source field name]; otherwise, <c>false</c>.
        /// </returns>
        public static bool HasAlteredSourceFieldName(this WorkItemChangedEvent workItemChangedEvent, AggregateRule rule)
        {
            return workItemChangedEvent.ChangedFields.StringFields.Any(f => f.ReferenceName.Equals(rule.Field.Source))
                   || workItemChangedEvent.ChangedFields.IntegerFields.Any(f => f.ReferenceName.Equals(rule.Field.Source));
        }

        /// <summary>
        /// Determines whether [the specified work item changed event] [has changed state inclusion].
        /// </summary>
        /// <param name="workItemChangedEvent">The work item changed event.</param>
        /// <param name="rule">The calculation rule.</param>
        /// <returns>
        /// <c>true</c> if [the specified work item changed event] [has changed state inclusion]; otherwise, <c>false</c>.
        /// </returns>
        public static bool HasAlteredInclusionStatus(this WorkItemChangedEvent workItemChangedEvent, AggregateRule rule)
        {
            var stateField =
                    workItemChangedEvent.ChangedFields.StringFields.FirstOrDefault(
                            sf => sf.ReferenceName.Equals(ConstantStrings.WorkItemStateCoreField));

            if (stateField == null)
            {
                return false;
            }

            var hasBeenExcluded = !rule.ExcludedSourceStates.Contains(stateField.OldValue)
                                  && rule.ExcludedSourceStates.Contains(stateField.NewValue);

            var hasBeenIncluded = rule.ExcludedSourceStates.Contains(stateField.OldValue)
                                  && !rule.ExcludedSourceStates.Contains(stateField.NewValue);

            return hasBeenExcluded || hasBeenIncluded;
        }

        /// <summary>
        /// Gets rule target work items that have been unlinked.
        /// </summary>
        /// <param name="workItem">The work item.</param>
        /// <param name="rule">The calculation rule.</param>
        /// <param name="workItemChangedEvent">The work item changed event.</param>
        /// <returns>
        /// A collection of work items that have been unlinked.
        /// </returns>
        public static IEnumerable<WorkItem> GetUnlinkedTargets(
                this WorkItem workItem, 
                AggregateRule rule,
                WorkItemChangedEvent workItemChangedEvent)
        {
            var contextFilter = string.Empty;

            if (rule.Link != null && workItemChangedEvent.DeletedRelations != null)
            {
                foreach (var deletedRelation in workItemChangedEvent.DeletedRelations)
                {
                    int id;
                    if (int.TryParse(deletedRelation.WorkItemId, out id))
                    {
                        contextFilter = string.Concat(
                                contextFilter, string.IsNullOrEmpty(contextFilter) ? string.Empty : ",", id);
                    }
                }
            }
            else if (workItemChangedEvent.ChangedFields != null && rule.InferredLink != null)
            {
                var changeField =
                        workItemChangedEvent.ChangedFields.StringFields.FirstOrDefault(
                                f => f.ReferenceName.Equals(rule.InferredLink.PathFieldName));

                if (changeField != null)
                {
                    contextFilter = changeField.OldValue;

                    switch (rule.InferredLink.MatchType)
                    {
                        case PathMatch.Partial:
                            contextFilter = GetPartialPath(contextFilter, rule.InferredLink.MatchDepth);
                            break;
                        case PathMatch.Complete:

                            // Strip any leading slashes
                            while (contextFilter.StartsWith("\\", StringComparison.Ordinal))
                            {
                                contextFilter = contextFilter.Substring(1);
                            }

                            break;
                    }
                }
            }

            return string.IsNullOrEmpty(contextFilter)
                           ? new WorkItem[] { }
                           : workItem.Store.Query(rule.CreateQuery(contextFilter, false)).OfType<WorkItem>();
        }

        /// <summary>
        /// Gets the linked work items that match the specifeid rule target conditions.
        /// </summary>
        /// <param name="workItem">The work item.</param>
        /// <param name="rule">The calculation rule.</param>
        /// <returns>
        /// Linked work items that match the rules target conditions.
        /// </returns>
        public static IEnumerable<WorkItem> GetLinkedTargets(this WorkItem workItem, AggregateRule rule)
        {
            return GetLinkedWorkItems(workItem, rule, false);
        }

        /// <summary>
        /// Gets the linked work items that match the specifeid rule source conditions.
        /// </summary>
        /// <param name="workItem">The work item.</param>
        /// <param name="rule">The calculation rule.</param>
        /// <returns>
        /// Linked work items that match the specifed rule source conditions.
        /// </returns>
        public static IEnumerable<WorkItem> GetLinkedSources(this WorkItem workItem, AggregateRule rule)
        {
            return GetLinkedWorkItems(workItem, rule, true);
        }

        /// <summary>
        /// Gets the linked work items that match the rule conditions.
        /// </summary>
        /// <param name="workItem">The work item.</param>
        /// <param name="rule">The calc rule.</param>
        /// <param name="getSources">if set to <c>true</c> [get sources].</param>
        /// <returns>get linked sources</returns>
        private static IEnumerable<WorkItem> GetLinkedWorkItems(WorkItem workItem, AggregateRule rule, bool getSources)
        {
            var query = string.Empty;

            if (rule.Link != null)
            {
                query = GetLinkQuery(rule, workItem, getSources);
            }
            else if (rule.InferredLink != null)
            {
                query = GetInferredLinkQuery(rule, workItem, getSources);
            }

            return string.IsNullOrEmpty(query)
                           ? new WorkItem[] { }
                           : workItem.Store.Query(query).OfType<WorkItem>();
        }

        /// <summary>
        /// Gets the link query.
        /// </summary>
        /// <param name="rule">The calc rule.</param>
        /// <param name="workItem">The work item.</param>
        /// <param name="getSourceItems">if set to <c>true</c> [get source items].</param>
        /// <returns>WIQL query string.</returns>
        private static string GetLinkQuery(AggregateRule rule, WorkItem workItem, bool getSourceItems)
        {
            var isForwardLink = getSourceItems ? rule.Link.IsTargetOnForwardLink : !rule.Link.IsTargetOnForwardLink;

            var relatedLinksIds =
                    workItem.Links.OfType<RelatedLink>().Where(
                            rl =>
                            rl.LinkTypeEnd.LinkType.ReferenceName.Equals(rule.Link.LinkTypeName)
                            && (!rl.LinkTypeEnd.LinkType.IsDirectional || rl.LinkTypeEnd.IsForwardLink == isForwardLink))
                            .Select(rl => rl.RelatedWorkItemId).ToArray();

            if (relatedLinksIds.Count() == 0)
            {
                return string.Empty;
            }

            var concatIds = string.Empty;
            foreach (var id in relatedLinksIds)
            {
                concatIds = string.Concat(concatIds, string.IsNullOrEmpty(concatIds) ? string.Empty : ",", id);
            }

            return rule.CreateQuery(concatIds, getSourceItems);
        }

        /// <summary>
        /// Gets the inferred link query.
        /// </summary>
        /// <param name="rule">The calc rule.</param>
        /// <param name="workItem">The work item.</param>
        /// <param name="getSourceItems">if set to <c>true</c> [get source items]; otherwise [get target items].</param>
        /// <returns>get inferred link query</returns>
        private static string GetInferredLinkQuery(AggregateRule rule, WorkItem workItem, bool getSourceItems)
        {
            var path = rule.InferredLink.PathType == LinkPath.IterationPath
                               ? workItem.IterationPath
                               : workItem.AreaPath;

            // If we want to match a partial path...
            if (rule.InferredLink.MatchType == PathMatch.Partial)
            {
                path = GetPartialPath(path, rule.InferredLink.MatchDepth);
            }

            return string.IsNullOrEmpty(path) 
                           ? string.Empty 
                           : rule.CreateQuery(path, getSourceItems);
        }

        /// <summary>
        /// Gets the partial path.
        /// </summary>
        /// <param name="path">The node path.</param>
        /// <param name="depth">The match depth.</param>
        /// <returns>The partial path to the specified depth.</returns>
        private static string GetPartialPath(string path, int depth)
        {
            var pathNodes = path.Split("\\".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

            path = pathNodes.Length >= depth
                           ? string.Concat(pathNodes.Take(depth).Select(s => string.Concat(s, "\\")).ToArray()) 
                           : string.Empty;
            return path;
        }

        /// <summary>
        /// Gets the source WIQL query.
        /// </summary>
        /// <param name="rule">The calc rule.</param>
        /// <param name="contextFilter">The filter.</param>
        /// <param name="getSourceItems">if set to <c>true</c> [get source].</param>
        /// <returns>WIQL for matched work items.</returns>
        private static string CreateQuery(this AggregateRule rule, string contextFilter, bool getSourceItems)
        {
            var query = getSourceItems ? rule.SourceWorkItemQuery : rule.TargetWorkItemQuery;
            return string.Format(CultureInfo.InvariantCulture, query, contextFilter);
        }
   }
}